package org.vaadin.elements.impl; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.jsoup.Jsoup; import org.jsoup.nodes.Attribute; import org.jsoup.nodes.Document; import org.vaadin.elements.ElementIntegration; import org.vaadin.elements.Elements; import org.vaadin.elements.Node; import org.vaadin.elements.Root; import org.vaadin.elements.TextNode; import com.vaadin.server.EncodeResult; import com.vaadin.server.JsonCodec; import com.vaadin.ui.Component; import com.vaadin.ui.JavaScriptFunction; import elemental.json.Json; import elemental.json.JsonArray; import elemental.json.JsonObject; import elemental.json.JsonType; import elemental.json.JsonValue; public class RootImpl extends ElementImpl implements Root { private ElementIntegration owner; private int callbackIdSequence = 0; private int nodeIdSequence = 0; private int fetchCallbackSequence = 0; private final Map<NodeImpl, Integer> nodeToId = new HashMap<>(); private final Map<Integer, NodeImpl> idToNode = new HashMap<>(); private JsonArray pendingCommands = Json.createArray(); private final Map<Integer, Runnable> fetchDomCallbacks = new HashMap<>(); private final Map<Integer, Component[]> fetchDomComponents = new HashMap<>(); public RootImpl(ElementIntegration owner) { super(new org.jsoup.nodes.Element(org.jsoup.parser.Tag.valueOf("div"), "")); Context context = new Context() { @Override protected void adopt(NodeImpl node) { super.adopt(node); if (node != RootImpl.this) { adoptNode(node); } } @Override protected void remove(NodeImpl node) { addCommand("remove", node); Integer id = nodeToId.remove(node); idToNode.remove(id); super.remove(node); } @Override public RootImpl getRoot() { return RootImpl.this; } }; this.owner = owner; context.adopt(this); Integer ownId = Integer.valueOf(0); nodeToId.put(this, ownId); idToNode.put(ownId, this); } private void addCommand(String name, Node target, JsonValue... params) { assert target == null || target.getRoot() == this; JsonArray c = Json.createArray(); c.set(0, name); if (target != null) { c.set(1, nodeToId.get(target).doubleValue()); } Arrays.asList(params).forEach(p -> c.set(c.length(), p)); pendingCommands.set(pendingCommands.length(), c); owner.markAsDirty(); } private void adoptNode(NodeImpl child) { // Even numbers generated by server, odd by client nodeIdSequence += 2; Integer id = Integer.valueOf(nodeIdSequence); adoptNode(child, id); } private void adoptNode(NodeImpl child, Integer id) { nodeToId.put(child, id); idToNode.put(id, child); // Enqueue initialization operations if (child instanceof ElementImpl) { ElementImpl e = (ElementImpl) child; addCommand("createElement", child, Json.create(e.getTag())); e.getAttributeNames().forEach(name -> setAttributeChange(e, name)); e.flushCommandQueues(); } else if (child instanceof TextNodeImpl) { TextNode t = (TextNode) child; addCommand("createText", child, Json.create(t.getText())); } else { throw new RuntimeException( "Unsupported node type: " + child.getClass()); } // Finally add append command addCommand("appendChild", child.getParent(), Json.create(id.doubleValue())); } void setAttributeChange(ElementImpl element, String name) { String value = element.getAttribute(name); if (value == null) { addCommand("removeAttribute", element, Json.create(name)); } else { addCommand("setAttribute", element, Json.create(name), Json.create(value)); } } public void setTextChange(TextNodeImpl textNode, String text) { addCommand("setText", textNode, Json.create(text)); } public JsonArray flushPendingCommands() { for (Entry<Integer, Component[]> entry : fetchDomComponents .entrySet()) { JsonArray connectorsJson = Json.createArray(); for (Component component : entry.getValue()) { connectorsJson.set(connectorsJson.length(), component.getConnectorId()); } addCommand("fetchDom", null, Json.create(entry.getKey().intValue()), connectorsJson); } fetchDomComponents.clear(); JsonArray payload = pendingCommands; pendingCommands = Json.createArray(); return payload; } void eval(ElementImpl element, String script, Object[] arguments) { // Param values JsonArray params = Json.createArray(); // Array of param indices that should be treated as callbacks JsonArray callbacks = Json.createArray(); for (int i = 0; i < arguments.length; i++) { Object value = arguments[i]; Class<? extends Object> type = value.getClass(); if (JavaScriptFunction.class.isAssignableFrom(type)) { // TODO keep sequence per element instead of "global" int cid = callbackIdSequence++; element.setCallback(cid, (JavaScriptFunction) value); value = Integer.valueOf(cid); type = Integer.class; callbacks.set(callbacks.length(), i); } EncodeResult encodeResult = JsonCodec.encode(value, null, type, null); params.set(i, encodeResult.getEncodedValue()); } addCommand("eval", element, Json.create(script), params, callbacks); } public void handleCallback(JsonArray arguments) { JsonArray attributeChanges = arguments.getArray(1); for (int i = 0; i < attributeChanges.length(); i++) { JsonArray attributeChange = attributeChanges.getArray(i); int id = (int) attributeChange.getNumber(0); String attribute = attributeChange.getString(1); JsonValue value = attributeChange.get(2); NodeImpl target = idToNode.get(Integer.valueOf(id)); if (value.getType() == JsonType.NULL) { target.node.removeAttr(attribute); } else { target.node.attr(attribute, value.asString()); } } JsonArray callbacks = arguments.getArray(0); for (int i = 0; i < callbacks.length(); i++) { JsonArray call = callbacks.getArray(i); int elementId = (int) call.getNumber(0); int cid = (int) call.getNumber(1); JsonArray params = call.getArray(2); ElementImpl element = (ElementImpl) idToNode .get(Integer.valueOf(elementId)); if (element == null) { System.out.println(cid + " detached?"); return; } JavaScriptFunction callback = element.getCallback(cid); callback.call(params); } } @Override public String asHtml() { StringBuilder b = new StringBuilder(); b.append(super.asHtml()); return b.toString(); } public void synchronize(int id, JsonArray hierarchy) { synchronizeRecursively(hierarchy, this); // Detach all removed nodes List<NodeImpl> detached = new ArrayList<>(); for (NodeImpl node : new ArrayList<>(idToNode.values())) { NodeImpl parent = node; while (parent != this) { if (parent == null) { detached.add(node); break; } parent = (NodeImpl) parent.getParent(); } } Context removedContext = new Context(); detached.forEach(node -> removedContext.adopt(node)); // Don't send the adopted structure to the client pendingCommands = Json.createArray(); Runnable callback = fetchDomCallbacks.remove(Integer.valueOf(id)); if (callback != null) { callback.run(); } } public void init(String html) { // Clear state removeAllChildren(); getAttributeNames().forEach(this::removeAttribute); nodeIdSequence = 2; Document bodyFragment = Jsoup.parseBodyFragment(html); List<org.jsoup.nodes.Node> childNodes = bodyFragment.body() .childNodes(); assert childNodes.size() == 1; org.jsoup.nodes.Node rootNode = childNodes.get(0); while (rootNode.childNodeSize() != 0) { org.jsoup.nodes.Node child = rootNode.childNode(0); ((org.jsoup.nodes.Element) node).appendChild(child); } context.wrapChildren(this); for (Attribute a : rootNode.attributes()) { setAttribute(a.getKey(), a.getValue()); } // Don't send the adopted structure to the client pendingCommands = Json.createArray(); // TODO sync ids fetchDomCallbacks.values().forEach(Runnable::run); fetchDomCallbacks.clear(); } private void synchronizeRecursively(JsonArray hierarchy, ElementImpl element) { int firstChild; JsonValue maybeAttributes = hierarchy.get(2); if (maybeAttributes.getType() == JsonType.OBJECT) { firstChild = 3; JsonObject attributes = (JsonObject) maybeAttributes; String[] names = attributes.keys(); HashSet<String> oldAttributes = new HashSet<>( element.getAttributeNames()); oldAttributes.removeAll(Arrays.asList(names)); oldAttributes.forEach(n -> element.removeAttribute(n)); Arrays.stream(names).forEach(name -> element.setAttribute(name, attributes.getString(name))); } else { firstChild = 2; } ArrayList<NodeImpl> newChildren = new ArrayList<>(); for (int i = firstChild; i < hierarchy.length(); i++) { JsonArray child = hierarchy.getArray(i); int nodeId; NodeImpl childNode; switch (child.get(0).getType()) { case NUMBER: TextNodeImpl textNode; nodeId = (int) child.getNumber(0); String text = child.getString(1); if (nodeId % 2 == 0) { // old node textNode = (TextNodeImpl) idToNode .get(Integer.valueOf(nodeId)); textNode.setText(text); } else { // new node textNode = (TextNodeImpl) Elements.createText(text); } childNode = textNode; break; case STRING: // Element node ElementImpl childElement; String tag = child.getString(0); nodeId = (int) child.getNumber(1); if (nodeId % 2 == 0) { // old node childElement = (ElementImpl) idToNode .get(Integer.valueOf(nodeId)); assert childElement.getTag().equals(tag); } else { // new node childElement = (ElementImpl) Elements.create(tag); } synchronizeRecursively(child, childElement); childNode = childElement; break; default: throw new RuntimeException( "Unsupported child JSON: " + child.toJson()); } if (nodeId % 2 == 1) { context.adopt(childNode); adoptNode(childNode, nodeId); } newChildren.add(childNode); } element.resetChildren(newChildren); } @Override public void fetchDom(Runnable callback, Component... connectorsToInlcude) { assert callback != null; Integer id = Integer.valueOf(fetchCallbackSequence++); fetchDomCallbacks.put(id, callback); fetchDomComponents.put(id, connectorsToInlcude); owner.markAsDirty(); } void setAttributeBound(ElementImpl elementImpl, String attributeName, String eventName) { addCommand("bindAttribute", elementImpl, Json.create(attributeName), Json.create(eventName)); } }